﻿# -*- coding: utf-8 -*-
#############################
# Módulo:   spell_corrector.py
# Autor:    Pablo Andrés Paliza
# Fecha:    2014/09
# Versión:  1.0
#
# Descripción: Modulo de python para corregir archivos dentro de un directorio
# basado en diccionarios.
# El archivo default.cfg tiene la configuracion predeterminada del directorio donde se encuentran
# los archivos a corregir, el directorio donde estan los diccionarios, si se va a corregir
# recursivamente los directorios y el nombre del archivo donde se van a guardar los resultados.
# Modo de uso:
# python spell_corrector {options}
#    -h, --help                 Muestra esta ayuda. 
#    -r, --recursive            Indica que el directorio de entrada se va a explorar recursivamente
#    -d, --dictionaries_folder  Recibe como parametro el directorio donde se encuentran los diccionarios a utilizar
#    -f, --folder_to_correct    Recibe como parametro el directorio de entrada de donde se van a leer los documentos a corregir 
#    -l, --log_filename         Recibe como parametro el nombre del archivo donde se va a guardar los resultados 
#    -g, --garbage_remove        Indica que los archivos corregidos no van a tener las cadenas identificadas como "basura"
#    -n, --name_detection        Detecta nombres propios y los corrige con un diccionario especifico
###############################

from __future__ import division

import sys
import getopt
import re
import collections
import codecs
import os
import unicodedata
import time
import json
import fnmatch #Para usar directorios recursivos
import ConfigParser 
from datetime import datetime


#Variables globales, son inicializadas en el metodo load_default_options
F_remove_garbage = False
F_recursive = False
F_name_detection= False
dictionary_path = ""
input_directory = ""
log_filename = ""
alphabet=""
classes =""
#diccionarios
PRE_PROCCESS= []
NWORDS = []
NWORDS_NO_NAMES =[] 
NNAMES= []



def load_default_options():
    global dictionary_path 
    global input_directory 
    global log_filename
    global F_recursive
    global F_remove_garbage
    global F_name_detection 
    global alphabet    
    global classes
    config = ConfigParser.ConfigParser()   
    config.readfp(codecs.open("default.cfg", "r", "utf8"))
    F_remove_garbage = config.getboolean("Flags", "F_remove_garbage")
    F_recursive =      config.getboolean("Flags", "F_recursive")
    F_name_detection =     config.getboolean("Flags", "F_name_detection")
    dictionary_path = config.get("Paths", "dictionary_path")
    input_directory = config.get("Paths", "input_directory")    
    log_filename = config.get("Paths", "log_filename")
    print log_filename
    print input_directory    
    #Agrego un time span al nombre por defecto para que no se pise y quede todo guardado
    log_filename= log_filename + "_{:%Y%m%d%H%M%S}".format(datetime.now()) + ".log"
    alphabet = config.get("Alphabet", "alphabet")
    classes =config.get("Alphabet", "classes")

PREVIOUS_IS_NAME = False
#Palabras en el archivo a corregir
FILE_WORDS = []
#Indice de iteracion actual en la listas de palabras del archivo a corregir
CURRENT_INDEX=0
    


#Cargo los diccionarios
def load_dictionaries():
    global PRE_PROCCESS
    global NWORDS 
    global NWORDS_NO_NAMES 
    global NNAMES
    
    json_data = open(os.path.join(dictionary_path ,'preProccess.json'))
    PRE_PROCCESS = json.load(json_data)
    
    json_data = open(os.path.join(dictionary_path ,'dictClean.json'))
    NWORDS = json.load(json_data)
    
    json_data = open(os.path.join(dictionary_path ,'dictCleanNoNames.json'))
    NWORDS_NO_NAMES = json.load(json_data)
    
    json_data = open(os.path.join(dictionary_path ,'namesNew.json'))
    NNAMES= json.load(json_data)




#print "Dictionary Load Time: "+ str(time.time() - t1)

#NWORDS = ''



#Retorna un conjunto de todas las cadenas en que cada letra de word es remplazada por todos los caracteres de
#su clase de similitud. Donde 'a' es el inicio de la palabra, 'b' es el pivot que se remplaza por 'r' que toma todos 
#los valores de la clase de similitud del caracter 'b', y 'c' es el final de la palabra (todos los posibles)
def edits_sim(word):
    splits     = [(word[:i],word[i],word[i+1:]) for i in range(len(word) )]
    replaces   = [a + r + c for a, b, c in splits for r in class_of(b)]
    return set(replaces)
#Retorna un conjunto de todas las cadenas  que estan a distancia de edicion 1 de word.
#Donde 'a' es el inicio de la palabra, b es el final y 'c' es el caracter que se utiliza para insertar un caracter 
# aleatorio del alfabeto o borrar alguno de los caracteres de la palabra word  
def edits(word):
    splits     = [(word[:i], word[i:]) for i in range(len(word) + 1)]
    deletes    = [a + b[1:] for a, b in splits if b]    
    replaces   = [a + c + b[1:] for a, b in splits for c in alphabet if b]
    inserts    = [a + c + b     for a, b in splits for c in alphabet]
    return set(deletes + replaces + inserts)

#Palabras que estan en el diccionario a 'distancia 2' (solamente usando reemplazos de su
#clase de similitud) de la palabra original 
def known_edits2_sim(word,dict):
    return set(e2 for e1 in edits_sim(word) for e2 in edits_sim(e1) if e2 in dict)
  
#Palabras que estan en el diccionario a 'distancia 2' de la palabra original    
def known_edits2(word,pdict):
    return set(e2 for e1 in edits(word) for e2 in edits(e1) if e2 in pdict)
#Palabras que estan en el diccionario a 'distancia 1' de la palabra original 
def known(words,pdict): return set(w for w in words if w in pdict)

# Retorna la misma palabra si pword esta en pdict. De no estar busca la palabra con mas frecuencia 
# que este a 'distancia 1' usando clases de similitud. Si no hay ninguna utiliza la misma formula sucesivamente para,
# buscar las palabras a 'distancia 1', 'distancia 2' usando clases de similitud,o 'distancia 2'
# De no encontrar ninguna palabra valida retorna pword
def correct(pword,pdict):
    word=pword.lower()	
    candidates =known([word],pdict)
    if len(candidates)==0:       
        candidates= known(edits_sim(word),pdict)
        if len(candidates)==0:
            candidates= known(edits(word),pdict)
            if len(candidates)==0:                
                candidates= known_edits2_sim(word,pdict)
                if len(candidates)==0:
                    candidates =known_edits2(word,pdict)
                    if len(candidates)==0:
                        candidates=[word]    
    ret = max(candidates, key=NWORDS.get)   
    return ret

# La mayoria de las palabras en mayusculas estan sin acentos en los documentos, pero en algunos casos el OCR los agrega
def remove_accents(s):
    s=s.replace(u"Á","A")
    s=s.replace(u"É","E")
    s=s.replace(u"Í","I")	
    s=s.replace(u"Ó","O")
    s=s.replace(u"Ú","U")
    return s
# Listado de palabras especiales que tienen asignadas correcciones predefinidas en el archivo preProccess.json
def pre_proccess(pword):
    if pword in PRE_PROCCESS:
        return PRE_PROCCESS[pword]
        
    n="º"
    if len(pword)== 2:
        if pword[0].isdigit() and  pword[1] == "?":
            return pword[0] + n.decode('utf-8')
    if len(pword)== 3:
        if pword[0].isdigit() and  pword[1] == "?" and  pword[2] == ",":
            return pword[0] + n.decode('utf-8') + pword[2]
    if len(pword)== 2:    
        if pword[0].isdigit() and  pword[1] == "\"":
            return pword[0] + n.decode('utf-8')
    if len(pword)== 3:
        if pword[0].isdigit() and  pword[1] == "\"" and  pword[2] == ",":
            return pword[0] + n.decode('utf-8') + pword[2]    
    return pword

#verifica si la palabra es 'basura'    
def is_not_a_word(pstr):
    count = 0
    # Rule 'L': "If a string is longer than 40 characters, it is garbage, If len<=2 the string is ignored"
    if len(pstr)>= 40 or len(pstr)<=2:
        return 1;
    # Rule 'R': "If a string has 4 identical characters in a row, it is garbage"      
    if re.search(r"(\w)\1{3,}",pstr):
        return 1 
    if len(pstr)==2 and (pstr[0] not in alphabet  and pstr[1] not in alphabet) or pstr.isdigit():
        return 1
    for c in pstr:
        if not c.isalpha():
            count=count+1            
    if count >= (len(pstr)/2):
        return 1
    return 0

    
#Recives a character and returns the similarity class
def class_of(ch):     
    for _class in classes.split():
        if ch in _class:
            return  _class    
    return ch

#Si es posiblemente un nombre lo corrigo con el dict de nombres, si no lo corrigo con el dict comun   
def try_correct_name(word):
    global PREVIOUS_IS_NAME
    ret=word     
    #Chequeo que no sea una palabra del diccionario    
    if len(known([word.lower()], NWORDS_NO_NAMES))==0:        
        if PREVIOUS_IS_NAME :
            ret =  correct(word,NNAMES)
            PREVIOUS_IS_NAME=True
            msg= "previous word is a name"            
        elif len(FILE_WORDS)>= CURRENT_INDEX+2:            
            if FILE_WORDS[CURRENT_INDEX+1][0].isupper():                
                if len (known([FILE_WORDS[CURRENT_INDEX+1].lower()], NNAMES))>0 :
                    ret =  correct(word,NNAMES)                    
                    PREVIOUS_IS_NAME=True
                    msg= "next word is a name"        
        if ret.lower()==word.lower():
            if len (known([word.lower()], NNAMES))==0:
                ret = correct(word,NWORDS)               
                PREVIOUS_IS_NAME=False
            else:
                PREVIOUS_IS_NAME=True
        else:
            #warning Just for testing, 
            log("::::::::######################:::::::::::::::::")            
            log("Name Detected -> Correction:" + word +" -> "+ ret ) 
            log(msg)           
            log("Previous WORD :: Name :: Next Word   ::    " + FILE_WORDS[CURRENT_INDEX-1] +" :: "+ FILE_WORDS[CURRENT_INDEX] + " :: "+ FILE_WORDS[CURRENT_INDEX+1]   )
            log("::::::::######################:::::::::::::::::")
                               
    return ret


#Procesa la palabra word y busca una correccion.
def process_word(word):
    global PREVIOUS_IS_NAME
    rword=pre_proccess(word)
    if rword == word:
        if is_not_a_word(word):
            if F_remove_garbage:
                rword=""
            else:
                rword= word
        else:
            if F_name_detection:
                if word[0].isupper(): #Si empieza con mayuscula puede ser un nombre
                    word_checked =  try_correct_name(word)
                else:                
                    PREVIOUS_IS_NAME=False
                    word_checked = correct(word,NWORDS)
            else:
                word_checked = correct(word,NWORDS)
            if word_checked == word.lower():
                rword= word
            else:
                if word.isupper():
                    rword= remove_accents( word_checked.upper())				
                elif word[0].isupper():
                    rword= word_checked[0].upper() + word_checked[1:]
                else:
                    rword= word_checked;
    if rword != word:
        log("Original -> Correction: "+ word+ " -> "+rword)
        print word + " -> " + rword
    return rword    


def correct_file(fpath):
    global PREVIOUS_IS_NAME
    global CURRENT_INDEX 
    global FILE_WORDS
    CURRENT_INDEX=0    
    print fpath    
    t1 = time.time()
    fpath_out = fpath.replace('.txt','.corrected.txt')
    fout= codecs.open(fpath_out,'w','utf_8')
    PREVIOUS_IS_NAME=False      
    with codecs.open(fpath,'r','utf_8') as vfile:
        content = vfile.read()        
        FILE_WORDS=re.findall('['+alphabet+'\-]+', content,re.IGNORECASE)
        for word in FILE_WORDS:
            cword = process_word(word)
            CURRENT_INDEX= CURRENT_INDEX+1
            if word != cword:
                content.replace(word,cword)
    fout.write(content)
    fout.close()
    log ("Correct_File Time : "+ str(time.time() - t1))
            
    
   
#Correct all the files in the directory that ends with .txt (ignores files Fixed.txt , manual.txt  and corrected.txt)
def correct_directory():          
    d=input_directory     
    if F_recursive:
        ficheros = []
        for root, dirnames, filenames in os.walk(d):
            for filename in fnmatch.filter(filenames, '*.txt'):
                ficheros.append(os.path.join(root, filename))                
    else:
        log("Working folder: "+d)   
        ficheros = os.listdir(d)
    for f in ficheros:
        if f[-4:]=='.txt' and f[-6:]!='al.txt' and f[-6:]!='ed.txt':
            print f
            log("Begin file :::: "+f+":::::::::")
            correct_file(os.path.join(d,f))
            log("End file :::::: "+f+" :::::::::")
          
   
   

def log(vstr):
    try:
        vfile = codecs.open(os.path.join(".",log_filename), 'a','utf_8')
        vfile.write(vstr+'\n')
        vfile.close()
    except:
        print log_filename
        print "An error has ocurred at logging"
        return None	

def print_help():
    print """
Spell Corrector: Corrector basado en diccionarios.
    
    -h, --help                  Muestra esta ayuda. 
    -r, --recursive             Indica que el directorio de entrada se va a explorar recursivamente
    -d, --dictionaries_folder   Recibe como parametro el directorio donde se encuentran los diccionarios a utilizar
    -f, --folder_to_correct     Recibe como parametro el directorio de entrada de donde se van a leer los documentos a corregir 
    -l, --log_filename          Recibe como parametro el nombre del archivo donde se va a guardar los resultados 
    -g, --garbage_remove        Indica que los archivos corregidos no van a tener las cadenas identificadas como "basura"
    -n, --name_detection        Detecta nombres propios y los corrige con un diccionario especifico
    """
    sys.exit(0)

def main(argv):
    global dictionary_path 
    global input_directory 
    global log_filename
    global F_recursive
    global F_remove_garbage
    global F_name_detection
    load_default_options() 
    try:
        opts, args = getopt.getopt(sys.argv[1:],"hnrgd:f:l:",["help=","name_detection","recursive=","garbage_remove","dictionaries_folder=","folder_to_correct=","log_filename="])
    except getopt.GetoptError:
        print_help()
    for opt, arg in opts:
        if opt == '-h':
            print_help()           
        elif opt in ("-d", "--dictionaries_folder"):
            dictionary_path = arg       
        elif opt in ("-f", "--folder_to_correct"):
            input_directory = arg
        elif opt in ("-l", "--log_filename"):
            log_filename = arg
        elif opt in ("-r", "--recursive"):
            F_recursive = True            
        elif opt in ("-g", "--garbage_remove"):
            F_remove_garbage = True
        elif opt in ("-n", "--name_detection"):
            F_name_detection  = True
    load_dictionaries()        
    correct_directory()
if __name__ == "__main__":
    main(sys.argv[1:])








#############################################TEST##############################################


                    
